<!--
@license
Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
-->
<link rel="import" href="../path.html">
<script>

  /**
   * The `Polymer.Templatizer` behavior adds methods to generate instances of
   * templates that are each managed by an anonymous `Polymer.Base` instance.
   *
   * Example:
   *
   *     // Get a template from somewhere, e.g. light DOM
   *     var template = Polymer.dom(this).querySelector('template');
   *     // Prepare the template
   *     this.templatize(template);
   *     // Instance the template with an initial data model
   *     var instance = this.stamp({myProp: 'initial'});
   *     // Insert the instance's DOM somewhere, e.g. light DOM
   *     Polymer.dom(this).appendChild(instance.root);
   *     // Changing a property on the instance will propagate to bindings
   *     // in the template
   *     instance.myProp = 'new value';
   *
   * Users of `Templatizer` may need to implement the following abstract
   * API's to determine how properties and paths from the host should be
   * forwarded into to instances:
   *
   *     _forwardParentProp: function(prop, value)
   *     _forwardParentPath: function(path, value)
   *
   * Likewise, users may implement these additional abstract API's to determine
   * how instance-specific properties that change on the instance should be
   * forwarded out to the host, if necessary.
   *
   *     _forwardInstanceProp: function(inst, prop, value)
   *     _forwardInstancePath: function(inst, path, value)
   *
   * In order to determine which properties are instance-specific and require
   * custom forwarding via `_forwardInstanceProp`/`_forwardInstancePath`,
   * define an `_instanceProps` map containing keys for each instance prop,
   * for example:
   *
   *     _instanceProps: {
   *       item: true,
   *       index: true
   *     }
   *
   * Any properties used in the template that are not defined in _instanceProp
   * will be forwarded out to the host automatically.
   *
   * Users should also implement the following abstract function to show or
   * hide any DOM generated using `stamp`:
   *
   *     _showHideChildren: function(shouldHide)
   *
   * @polymerBehavior
   */
  Polymer.Templatizer = {

    properties: {
      __hideTemplateChildren__: {
        observer: '_showHideChildren'
      }
    },

    // Extension point for overrides
    _instanceProps: Polymer.nob,

    _parentPropPrefix: '_parent_',

    /**
     * Prepares a template containing Polymer bindings by generating
     * a constructor for an anonymous `Polymer.Base` subclass to serve as the
     * binding context for the provided template.
     *
     * Use `this.stamp` to create instances of the template context containing
     * a `root` fragment that may be stamped into the DOM.
     *
     * @method templatize
     * @param {HTMLTemplateElement} template The template to process.
     */
    templatize: function(template) {
      this._templatized = template;
      // TODO(sjmiles): supply _alternate_ content reference missing from root
      // templates (not nested). `_content` exists to provide content sharing
      // for nested templates.
      if (!template._content) {
        template._content = template.content;
      }
      // fast path if template's anonymous class has been memoized
      if (template._content._ctor) {
        this.ctor = template._content._ctor;
        //console.log('Templatizer.templatize: using memoized archetype');
        // forward parent properties to archetype
        this._prepParentProperties(this.ctor.prototype, template);
        return;
      }
      // `archetype` is the prototype of the anonymous
      // class created by the templatizer
      var archetype = Object.create(Polymer.Base);
      // normally Annotations.parseAnnotations(template) but
      // archetypes do special caching
      this._customPrepAnnotations(archetype, template);

      // forward parent properties to archetype
      this._prepParentProperties(archetype, template);

      // setup accessors
      archetype._prepEffects();
      this._customPrepEffects(archetype);
      archetype._prepBehaviors();
      archetype._prepPropertyInfo();
      archetype._prepBindings();

      // boilerplate code
      archetype._notifyPathUp = this._notifyPathUpImpl;
      archetype._scopeElementClass = this._scopeElementClassImpl;
      archetype.listen = this._listenImpl;
      archetype._showHideChildren = this._showHideChildrenImpl;
      archetype.__setPropertyOrig = this.__setProperty;
      archetype.__setProperty = this.__setPropertyImpl;
      // boilerplate code
      var _constructor = this._constructorImpl;
      var ctor = function TemplateInstance(model, host) {
        _constructor.call(this, model, host);
      };
      // standard references
      ctor.prototype = archetype;
      archetype.constructor = ctor;
      // TODO(sjmiles): constructor cache?
      template._content._ctor = ctor;
      // TODO(sjmiles): choose less general name
      this.ctor = ctor;
    },

    _getRootDataHost: function() {
      return (this.dataHost && this.dataHost._rootDataHost) || this.dataHost;
    },

    _showHideChildrenImpl: function(hide) {
      var c = this._children;
      for (var i=0; i<c.length; i++) {
        var n = c[i];
        // Ignore non-changes
        if (Boolean(hide) != Boolean(n.__hideTemplateChildren__)) {
          if (n.nodeType === Node.TEXT_NODE) {
            if (hide) {
              n.__polymerTextContent__ = n.textContent;
              n.textContent = '';
            } else {
              n.textContent = n.__polymerTextContent__;
            }
          } else if (n.style) {
            if (hide) {
              n.__polymerDisplay__ = n.style.display;
              n.style.display = 'none';
            } else {
              n.style.display = n.__polymerDisplay__;
            }
          }
        }
        n.__hideTemplateChildren__ = hide;
      }
    },

    __setPropertyImpl: function(property, value, fromAbove, node) {
      if (node && node.__hideTemplateChildren__ && property == 'textContent') {
        property = '__polymerTextContent__';
      }
      this.__setPropertyOrig(property, value, fromAbove, node);
    },

    _debounceTemplate: function(fn) {
      Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', fn));
    },

    _flushTemplates: function() {
      Polymer.dom.flush();
    },

    _customPrepEffects: function(archetype) {
      var parentProps = archetype._parentProps;
      for (var prop in parentProps) {
        archetype._addPropertyEffect(prop, 'function',
          this._createHostPropEffector(prop));
      }
      for (prop in this._instanceProps) {
        archetype._addPropertyEffect(prop, 'function',
          this._createInstancePropEffector(prop));
      }
    },

    _customPrepAnnotations: function(archetype, template) {
      archetype._template = template;
      var c = template._content;
      if (!c._notes) {
        var rootDataHost = archetype._rootDataHost;
        if (rootDataHost) {
          Polymer.Annotations.prepElement = function() {
            rootDataHost._prepElement();
          }
        }
        c._notes = Polymer.Annotations.parseAnnotations(template);
        Polymer.Annotations.prepElement = null;
        this._processAnnotations(c._notes);
      }
      archetype._notes = c._notes;
      archetype._parentProps = c._parentProps;
    },

    // Sets up accessors on the template to call abstract _forwardParentProp
    // API that should be implemented by Templatizer users to get parent
    // properties to their template instances.  These accessors are memoized
    // on the archetype and copied to instances.
    _prepParentProperties: function(archetype, template) {
      var parentProps = this._parentProps = archetype._parentProps;
      if (this._forwardParentProp && parentProps) {
        // Prototype setup (memoized on archetype)
        var proto = archetype._parentPropProto;
        var prop;
        if (!proto) {
          for (prop in this._instanceProps) {
            delete parentProps[prop];
          }
          proto = archetype._parentPropProto = Object.create(null);
          if (template != this) {
            // Assumption: if `this` isn't the template being templatized,
            // assume that the template is not a Poylmer.Base, so prep it
            // for binding
            Polymer.Bind.prepareModel(proto);
            Polymer.Base.prepareModelNotifyPath(proto);
          }
          // Create accessors for each parent prop that forward the property
          // to template instances through abstract _forwardParentProp API
          // that should be implemented by Templatizer users
          for (prop in parentProps) {
            var parentProp = this._parentPropPrefix + prop;
            // TODO(sorvell): remove reference Bind library functions here.
            // Needed for effect optimization.
            var effects = [{
              kind: 'function',
              effect: this._createForwardPropEffector(prop),
              fn: Polymer.Bind._functionEffect
            }, {
              kind: 'notify',
              fn: Polymer.Bind._notifyEffect,
              effect: {event:
                Polymer.CaseMap.camelToDashCase(parentProp) + '-changed'}
            }];
            proto._propertyEffects = proto._propertyEffects || {};
            proto._propertyEffects[parentProp] = effects;
            Polymer.Bind._createAccessors(proto, parentProp, effects);
          }
        }
        // capture this reference for use below
        var self = this;
        // Instance setup
        if (template != this) {
          Polymer.Bind.prepareInstance(template);
          template._forwardParentProp = function(source, value) {
            self._forwardParentProp(source, value);
          }
        }
        this._extendTemplate(template, proto);
        template._pathEffector = function(path, value, fromAbove) {
          return self._pathEffectorImpl(path, value, fromAbove);
        }
      }
    },

    _createForwardPropEffector: function(prop) {
      return function(source, value) {
        this._forwardParentProp(prop, value);
      };
    },

    _createHostPropEffector: function(prop) {
      var prefix = this._parentPropPrefix;
      return function(source, value) {
        this.dataHost._templatized[prefix + prop] = value;
      };
    },

    _createInstancePropEffector: function(prop) {
      return function(source, value, old, fromAbove) {
        if (!fromAbove) {
          this.dataHost._forwardInstanceProp(this, prop, value);
        }
      };
    },

    // Similar to Polymer.Base.extend, but retains any previously set instance
    // values (_propertySetter back on instance once accessor is installed)
    _extendTemplate: function(template, proto) {
      var n$ = Object.getOwnPropertyNames(proto);
      if (proto._propertySetter) {
        // _propertySetter API may need to be copied onto the template,
        // and it needs to come first to allow the property swizzle below
        template._propertySetter = proto._propertySetter;
      }
      for (var i=0, n; (i<n$.length) && (n=n$[i]); i++) {
        var val = template[n];
        if (val && n == '_propertyEffects') {
          // Merge property effects in
          var pe = Polymer.Base.mixin({}, val);
          template._propertyEffects = Polymer.Base.mixin(pe, proto._propertyEffects);
        } else {
          var pd = Object.getOwnPropertyDescriptor(proto, n);
          Object.defineProperty(template, n, pd);
          if (val !== undefined) {
            template._propertySetter(n, val);
          }   
        }
      }
    },

    // Extension points for Templatizer sub-classes
    /* eslint-disable no-unused-vars */
    _showHideChildren: function(hidden) { },
    _forwardInstancePath: function(inst, path, value) { },
    _forwardInstanceProp: function(inst, prop, value) { },
    // Defined-check rather than thunk used to avoid unnecessary work for these:
    // _forwardParentPath: function(path, value) { },
    // _forwardParentProp: function(prop, value) { },
    /* eslint-enable no-unused-vars */

    _notifyPathUpImpl: function(path, value) {
      var dataHost = this.dataHost;
      var root = Polymer.Path.root(path);
      // Call extension point for Templatizer sub-classes
      dataHost._forwardInstancePath.call(dataHost, this, path, value);
      if (root in dataHost._parentProps) {
        dataHost._templatized._notifyPath(dataHost._parentPropPrefix + path, value);
      }
    },

    // Overrides Base notify-path module
    _pathEffectorImpl: function(path, value, fromAbove) {
      if (this._forwardParentPath) {
        if (path.indexOf(this._parentPropPrefix) === 0) {
          var subPath = path.substring(this._parentPropPrefix.length);
          var model = Polymer.Path.root(subPath);
          if (model in this._parentProps) {
            this._forwardParentPath(subPath, value);
          }
        }
      }
      Polymer.Base._pathEffector.call(this._templatized, path, value, fromAbove);
    },

    _constructorImpl: function(model, host) {
      this._rootDataHost = host._getRootDataHost();
      this._setupConfigure(model);
      this._registerHost(host);
      this._beginHosting();
      this.root = this.instanceTemplate(this._template);
      this.root.__noContent = !this._notes._hasContent;
      this.root.__styleScoped = true;
      this._endHosting();
      this._marshalAnnotatedNodes();
      this._marshalInstanceEffects();
      this._marshalAnnotatedListeners();
      // each row is a document fragment which is lost when we appendChild,
      // so we have to track each child individually
      var children = [];
      for (var n = this.root.firstChild; n; n=n.nextSibling) {
        children.push(n);
        n._templateInstance = this;
      }
      // Since archetype overrides Base/HTMLElement, Safari complains
      // when accessing `children`
      this._children = children;
      // Ensure newly stamped nodes reflect host's hidden state
      if (host.__hideTemplateChildren__) {
        this._showHideChildren(true);
      }
      // ready self and children
      this._tryReady();
    },

    // Decorate events with model (template instance)
    _listenImpl: function(node, eventName, methodName) {
      var model = this;
      var host = this._rootDataHost;
      var handler = host._createEventHandler(node, eventName, methodName);
      var decorated = function(e) {
        e.model = model;
        handler(e);
      };
      host._listen(node, eventName, decorated);
    },

    _scopeElementClassImpl: function(node, value) {
      var host = this._rootDataHost;
      if (host) {
        return host._scopeElementClass(node, value);
      }
      return value;
    },

    /**
     * Creates an instance of the template previously processed via
     * a call to `templatize`.
     *
     * The object returned is an anonymous subclass of Polymer.Base that
     * has accessors generated to manage data in the template.  The DOM for
     * the instance is contained in a DocumentFragment called `root` on
     * the instance returned and must be manually inserted into the DOM
     * by the user.
     *
     * Note that a call to `templatize` must be called once before using
     * `stamp`.
     *
     * @method stamp
     * @param {Object=} model An object containing key/values to serve as the
     *   initial data configuration for the instance.  Note that properties
     *   from the host used in the template are automatically copied into
     *   the model.
     * @return {Polymer.Base} The Polymer.Base instance to manage the template
     *   instance.
     */
    stamp: function(model) {
      model = model || {};
      if (this._parentProps) {
        var templatized = this._templatized;
        for (var prop in this._parentProps) {
          if (model[prop] === undefined) {
            model[prop] = templatized[this._parentPropPrefix + prop];
          }
        }
      }
      return new this.ctor(model, this);
    },

    /**
     * Returns the template "model" associated with a given element, which
     * serves as the binding scope for the template instance the element is
     * contained in. A template model is an instance of `Polymer.Base`, and
     * should be used to manipulate data associated with this template instance.
     *
     * Example:
     *
     *   var model = modelForElement(el);
     *   if (model.index < 10) {
     *     model.set('item.checked', true);
     *   }
     *
     * @method modelForElement
     * @param {HTMLElement} el Element for which to return a template model.
     * @return {Object<Polymer.Base>} Model representing the binding scope for
     *   the element.
     */
    modelForElement: function(el) {
      var model;
      while (el) {
        // An element with a _templateInstance marks the top boundary
        // of a scope; walk up until we find one, and then ensure that
        // its dataHost matches `this`, meaning this dom-repeat stamped it
        if ((model = el._templateInstance)) {
          // Found an element stamped by another template; keep walking up
          // from its dataHost
          if (model.dataHost != this) {
            el = model.dataHost;
          } else {
            return model;
          }
        } else {
          // Still in a template scope, keep going up until
          // a _templateInstance is found
          el = el.parentNode;
        }
      }
    }

  };

</script>
